上一篇文章把开发 Agent 拆成了三层:基模型生成文本,应用层提供交互界面,中间层负责把模型的意图转换成具体的操作。这一篇专门讲中间层。
中间层是运行在基模型和应用层之间的一整套基础设施。它管四件事:模型每次能看到什么信息(上下文管理),模型能请求执行什么操作(工具调用),不同来源的工具和数据源怎么标准化接入(协议适配),以及哪些操作可以自动执行、哪些需要人类批准(权限控制)。这四个组件共同决定了 Agent 在工程场景里能走多远。

上下文:挑得准比塞得多重要

为什么不能把项目全塞进去

2026 年的基模型在上下文窗口上已经到了惊人的程度。Claude Opus 4.8 和 GPT-5.5 都是 1M token,DeepSeek V4 系列也是 1M。1M token 大概等于 75 万英文单词或 50 万中文字,相当于三本《三体》第一部的体量。一个典型的中型 React 项目,把 src 下所有 tsx 和 ts 文件拼在一起大概 5 到 10 万行,换算成 token 大概 15 到 30 万。从容量角度看,塞进去完全没问题。
但工程上不会这么做。原因有三个。
第一个是注意力衰减。语言模型处理长序列时,对中间位置的信息提取能力弱于开头和结尾。这是 Transformer 架构里注意力机制的内在特性,中间 token 的注意力权重天然被两端稀释。有一篇 2023 年的论文专门研究了这个问题,标题就叫 Lost in the Middle。把整个项目塞进去,模型可能对最前面和最后面的几个文件理解得很好,对中间几十个文件的理解质量明显下降。
第二个是信号噪声比。当前任务通常只需要项目中很小一部分代码。假设要修改用户认证逻辑,相关文件可能只有 auth.ts、session.ts 和 login 组件三个文件,加起来 500 行。把另外 5 万行不相关的 UI 组件、工具函数、测试代码也发过去,模型需要在噪音里自己判断哪些是相关的。它可能被看起来相关但实际无关的代码干扰,比如看到一个叫 validateCredentials 的函数在另一个完全不相关的模块里,误以为那也是认证逻辑的一部分。
第三个是成本和延迟。1M token 的输入,即使在便宜的模型上也是一笔开销。以 DeepSeek V4-Flash 每百万 token 0.14 美元算,每次请求 1M token 的成本是 0.14 美元。一个复杂任务可能涉及 50 到 100 次模型调用,每次都传全量上下文的话,单次任务的 API 费用轻松超过 10 美元。同样的任务,通过精准的上下文检索把每次请求控制在 30K token 以内,费用可以降到 0.5 美元以下。延迟方面,1M token 的推理时间比 30K token 长不少,生成第一个 token 的等待时间也明显增加。

检索:怎么找到该看的代码

既然不能全塞,就需要一套检索机制,在每次模型请求前挑出和当前任务最相关的代码片段。这个机制是上下文管理的核心。
关键词检索是最朴素也最可靠的方式,通常基于 BM25 或类似算法。BM25 是一种基于词频和逆文档频率的排序算法,它找的是包含搜索词的文档,但那些在所有文档里都频繁出现的常见词权重很低。搜 loginHandler,返回所有包含 loginHandler 这个字符串的文件,按相关度排序。关键词检索的优点是快、确定性强、不需要 GPU。缺点是不理解语义,搜身份验证找不到只写了 authentication 的文件,搜用户登录找不到只写了 signIn 的函数。
向量检索解决的就是语义匹配问题。流程是把代码片段通过一个 embedding 模型转成高维向量,存到向量数据库里。查询时把查询语句也转成向量,然后找向量空间里最接近的 K 个代码片段,通常用余弦相似度或欧氏距离衡量。向量检索的好处是能理解同义词和语义相近的表达,搜用户认证能找到 auth、login、signIn、credential 相关的代码,不管具体用词是什么。代价是可能返回语义相关但实际不相关的片段,需要后续过滤。embedding 模型的推理和向量搜索本身也消耗计算资源。
结构化检索利用编译器前端的技术,包括 AST(抽象语法树)和 LSP(语言服务器协议)。AST 把代码解析成树状结构,每个节点是一个语法单元:函数定义、变量声明、if 语句、import 语句等。LSP 在 AST 之上提供更多语义信息:跳转到定义、查找所有引用、获取类型信息、查看函数签名。有了这些,Agent 可以精确查找所有调用了 deleteUser 的地方、UserSession 类型的所有字段、某个接口的所有实现类。结构化检索的准确率最高,但依赖语言特定的工具链,项目需要能正常构建、LSP server 正常运行、索引是最新的。
混合检索是实际工程中最常见的方案。先用关键词和向量检索快速定位候选文件,再用 AST 和 LSP 提取精确的符号信息,最后根据依赖图和调用关系补全上下文边界。如果修改了函数 A 的签名,所有调用 A 的地方都在影响范围内,都应该进入上下文。

上下文组装

检索出来的代码片段不能直接拼在一起扔给模型。它们来自不同文件,有不同职责,缺少组织结构。模型需要知道这些片段之间的关系。
文件摘要是第一步。每个文件被检索出来后,Agent 先生成一个简要摘要,说明这个文件做什么、主要导出什么、依赖哪些模块。一个 50 字的摘要比一个 500 行的文件节省 95% 的空间。
依赖图是第二步。如果要修改 userService.ts,所有 import userService 的文件都在潜在影响范围内。依赖图不需要把所有文件内容都放进去,但至少要让模型知道修改这个函数会影响哪些文件,这样模型生成代码时会更谨慎,修改后知道需要检查哪些地方。
最近修改记录提供时间维度的信息。一个文件如果最近一周被频繁修改,可能包含正在进行的功能开发或 bug 修复,和当前任务的相关概率更高。一个文件如果已经一年没动过,更可能是稳定代码,修改它需要更充分的理由。
项目规范文件是元信息层。AGENTS.mdCLAUDE.md、.cursorrules 这些文件由开发者手动编写,告诉 Agent 项目的技术栈、代码风格、架构约定、测试策略、不能做的事。花 10 分钟写好 AGENTS.md,Agent 之后 100 次任务都会受益。规范文件是空的或者过时的,Agent 只能靠猜。
错误日志和测试输出提供运行时反馈。Agent 修改代码后运行测试,测试失败的错误信息会成为下一轮模型请求的上下文。这个反馈循环是 Agent 自我纠正的核心机制:看到自己引入的错误,分析原因,修改代码,再次运行测试,直到通过。没有这个循环,Agent 等于闭着眼睛写代码。

上下文压缩

一个复杂的 Agent 任务可能持续 20 到 30 分钟,经历 50 到 100 轮对话。每轮对话都包含模型的推理输出、工具调用请求、工具执行结果。累积到一定长度后,早期的上下文不再需要保留完整内容。比如第 3 轮读取了一个文件,到了第 30 轮,模型不需要再看到那个文件的完整内容,只需要知道在第 3 轮读了 auth.ts,发现了两个问题。
上下文压缩做的就是这件事:对早期工具调用的输出做摘要,保留关键信息如文件路径、发现的问题、做出的决策,丢弃细节如完整文件内容、原始终端输出,释放空间给当前轮次。
压缩策略的难点在于判断哪些信息将来还会用到。一个读文件的结果可能在 10 轮后被引用。一个失败的测试输出可能是后续修改的关键线索。过早压缩会丢失重要信息,过晚压缩会导致上下文溢出和成本失控。目前常见做法是保留最近 10 轮左右的完整上下文,更早的做摘要压缩,但保留模型明确标记为重要的信息。

工具调用:模型的手

工具怎么定义

模型本身只有语言能力。让它读写文件、执行命令、查询数据库,靠的是工具调用。
机制是这样的。宿主应用每次发给模型的请求里附带一个工具列表,每个工具用 JSON Schema 描述自己的名称、功能、参数和约束。模型生成回复时,如果判断需要调用某个工具,就不生成自然语言,而是生成一个结构化的工具调用请求。宿主应用收到请求后执行对应的函数,把结果返回给模型,模型根据结果决定下一步。
每个环节在实际工程中都有坑。工具 schema 的设计直接影响调用成功率。假设定义了一个数据库查询工具,description 写执行 SQL 查询,参数 sql 的 description 写 SQL 语句。这个定义有三个问题:description 太模糊,模型不知道这是只读还是可以写,不知道连的是哪个数据库,不知道表结构;参数没有约束,没限制必须是 SELECT,模型可能生成 INSERT 或 DROP;没有返回格式说明,模型不知道结果会以什么形式返回,无法规划后续操作。
一个更好的定义是这样的。工具名叫 query_database_readonly。description 写清楚:在项目的 SQLite 数据库上执行只读查询,数据库路径是 data/app.db,可用的表有 users(id, name, email, created_at)和 projects(id, title, owner_id, updated_at)。查询结果以 JSON 数组返回,最多 100 行。参数 sql 的 description 写清楚只允许 SELECT 或 EXPLAIN,禁止 INSERT、UPDATE、DELETE、DROP、ALTER,使用参数化查询语法如 $1、$2。再加一个 params 参数,类型是字符串数组,对应 SQL 里占位符的参数值。
这个版本给了模型足够的信息判断需要查什么、怎么查、查出来的结果什么样。参数约束和行数限制是工具层面的安全网。表结构的简要描述让模型不需要额外调 get_schema 就能规划查询。

调用流程中的工程问题

一次工具调用从模型生成请求到结果返回,中间经历多个环节,每个环节都可能失败。
参数校验是第一个环节。模型可能生成不合法的参数:类型不对、缺少必填字段、字符串超长、数值超出范围。宿主应用需要在执行前校验,失败时返回结构化的错误信息。参数 sql 的值包含 DROP 语句,本工具只允许 SELECT 和 EXPLAIN,这个错误信息比参数错误四个字有用得多。
超时处理是第二个环节。Shell 命令可能卡住,npm install 网络超时、test suite 跑了 10 分钟没结束、数据库查询锁表。每个工具调用都需要设超时阈值,超时后终止执行并返回超时状态。模型需要知道操作超时了,不能以为一切正常继续下一步。
失败重试是第三个环节。读操作失败通常可以安全重试。写操作的重试要谨慎,如果写文件操作因为超时被判定失败,但文件实际已经写入了一半,重试可能导致内容错乱。稳妥的做法是写操作不自动重试,返回状态给模型,让模型判断是否需要重新生成并再次写入。
审计日志是第四个环节。每次工具调用都需要记录时间戳、工具名称、输入参数(脱敏后)、输出结果(截断后)、执行耗时、成功或失败状态。这些日志在问题排查时能回溯 Agent 的每一步操作,知道它在第 27 轮为什么读了一个错误的文件,或者在哪一轮引入了 bug。

常见工具类型

开发 Agent 里常见的工具按功能分几类,不同工具有不同的安全风险。
文件系统工具:读文件、写文件、搜索文件内容、列出目录、获取文件元信息。最频繁调用的工具。读操作风险低,通常自动执行。写操作需要展示 diff。
Shell 工具:在终端执行任意命令。能力最大也最危险,几乎能做任何事。好的实践是区分只读命令和有副作用的命令。ls、cat、git status、git diff、git log 这些可以自动执行。rm、mv、npm install、git commit 这些需要确认。rm -rf、git push --force、sudo 这类已知危险命令应该拒绝执行或要求双重确认。
Git 工具:查看 diff、创建分支、暂存文件、提交、查看日志、切换分支。Git 操作的审计追溯尤其重要,涉及代码版本变更,出问题需要知道谁做了什么、为什么、在哪个 commit 里。
测试和检查工具:运行类型检查(tsc --noEmit、cargo check、mypy)、lint(eslint、clippy、ruff)、formatter(prettier、cargo fmt、black)、测试套件(pytest、cargo test、vitest、jest)。这些工具构成 Agent 的验证反馈循环。
浏览器工具:打开 URL、点击元素、读取页面文本、截屏、执行 JavaScript。用于需要 UI 交互验证的场景。涉及网络访问,有 SSRF 和数据外传风险。
API 工具:发送 HTTP 请求、设置请求头、解析响应。需要限制请求目标,通常只允许调用项目配置里声明的 API 端点,不允许任意 URL。
数据库工具:获取表结构、执行只读查询、explain 查询计划。权限和安全约束在后面详细讨论。

MCP:标准化的工具接入协议

MCP(Model Context Protocol)是 Anthropic 在 2024 年底提出的开放协议,后来逐渐被更多平台采纳。它的核心目标很简单:每个 Agent 工具都要为每个模型、每个平台重复开发,能不能只写一次。
MCP 的架构用三个角色来回答这个问题。MCP Server 负责暴露一组工具或资源,可以封装文件系统、数据库、第三方 API 或任何其他能力,向 Client 注册自己提供的工具列表和参数 schema。MCP Client 嵌入在宿主应用里(如 Claude Desktop、Claude Code),负责连接 Server、转发模型的工具调用请求、把执行结果返回给模型。宿主应用管理用户交互、权限审批和会话状态。
MCP 出现之前,想在 Claude Desktop 里查询 Postgres 数据库,需要自己写一个中间服务,在 Claude Desktop 的工具配置里注册它,处理认证和连接管理。MCP 出现之后,只需要安装 Postgres MCP Server,在配置文件中加一行连接字符串就行了。Server 的作者已经处理了连接池、查询执行、结果格式化。
这个模式的好处是工具生态可以跨应用复用。GitHub 官方维护的 MCP Server 在 Claude Desktop、Cursor、Continue 里都能用。社区贡献的 Server 覆盖了越来越多的服务:Jira、Slack、Figma、Notion、Google Drive、AWS、Kubernetes。开发者不需要为每个工具写适配代码。
但 MCP 有几个需要正视的局限。
权限模型在 MCP 协议层面是缺失的。一个 MCP Server 声称提供数据库查询功能,但 MCP 协议本身不强制这个 Server 只暴露只读操作。如果 Server 的实现允许 DROP TABLE,宿主应用在协议层面没办法阻止,只能依赖 Server 自己的实现质量。引入一个第三方 MCP Server 等于引入了那个 Server 的全部权限,需要像审查依赖库一样审查 MCP Server 的代码和行为。
Server 质量参差不齐。社区维护的 MCP Server 从精心编写到几小时拼凑的都有。质量差的 Server 可能有内存泄漏、连接未正确关闭、错误处理不完善、schema 描述不准确,导致模型调用频繁失败。排查这些问题很痛苦,看到的是模型调用工具失败,但根因在 MCP Server 的实现里,中间隔了模型、Client 和网络。
MCP 也不是唯一方案。OpenAI 的 Agents SDK 有自己的工具注册机制,用 Python 装饰器或函数引用直接注册。LangChain 和 LlamaIndex 也有各自的工具抽象层。很多 CLI Agent 直接在代码里注册工具函数,不走任何外部协议。MCP 的价值在于跨应用复用,但如果只需要在一个应用里用一组固定工具,直接在代码里注册可能更简单可控。

数据库连接:为什么不能差不多

模型会写 SQL。很多开发者第一次意识到这件事时,就让 Agent 直连数据库。这个冲动可以理解,既然信任 Agent 写代码,为什么不信任它写 SQL。
因为代码和数据库操作的风险模型完全不同。代码写错了,最坏是编译不过或测试挂了。SQL 写错了但语法正确,它会静默执行,删掉不该删的行,更新了不该更新的列,或者在百万行表上跑全表扫描把数据库 CPU 打满。代码有 git 可以回滚,数据库的回滚需要事先准备,有些 DDL 操作是隐式提交的,ROLLBACK 也挽回不了。
Agent 连接数据库的安全底线应该包括这些。
默认只读。数据库工具默认只接受 SELECT 和 EXPLAIN。任何写操作需要开发者显式开启写模式,并且每次写操作执行前展示影响范围的预估。
返回行数限制。即使 SELECT 也可能出问题,select * from logs 在 5000 万行的表上执行会打满网络和内存。默认限制 100 行,超出需要分页或聚合。
敏感字段脱敏。密码哈希、token、身份证号、手机号、银行卡号,这些字段的值在返回给模型之前就应该替换为占位符。模型不需要看到这些值来完成它的任务。
写操作审批和 diff 展示。UPDATE 或 INSERT 执行前,应该展示类似 git diff 的影响预览:哪些行会被修改、修改前后的值对比、影响行数。开发者确认后才执行。
迁移 dry-run 和回滚。数据库 schema 变更(ALTER TABLE、CREATE INDEX 等)必须先 dry-run 验证语法和影响范围,生成回滚脚本,开发者确认后才执行。Agent 不应自行决定修改生产库的 schema。
凭据隔离。Agent 不应持有生产库的连接字符串或凭据。凭据由宿主应用管理,Agent 通过受限的工具函数间接访问。对于桌面应用(Electron 或 Tauri),数据库操作在 main process 或 Rust backend 中执行,Agent 通过 IPC 或 command 调用。
这些约束不是过度工程。Agent 在调试问题时可能在一个循环里反复查询数据库,每次查询都不同,试图摸索数据规律。如果这些查询都是全表扫描,几分钟就能耗尽连接池。Agent 在修复数据不一致时,可能生成一个缺少 WHERE 子句的 UPDATE,因为模型在生成长 SQL 时注意力集中在列名和值上,漏掉了过滤条件。安全约束就是在这些意外发生之前切断破坏路径。

权限、沙箱和可观测性

权限设计是梯度题,不同操作的风险等级不同,授权策略也应该不同。
文件读取通常风险最低。Agent 读文件不会改变状态,最坏情况是读到了敏感信息如 .env 里的 API key,然后写到了别处。文件读取的权限可以宽松,但需要配合写操作审查防止信息泄漏。
文件写入风险中等。写错了可以通过 git checkout 恢复,前提是文件被 git 追踪。未被追踪的新文件、.gitignore 里的配置文件、二进制文件,写错了恢复更麻烦。好的实践是 Agent 写文件后自动展示 diff,写入非 git 追踪的文件需要额外确认。
Shell 命令执行的风险跨度极大。git status 和 rm -rf 都是 Shell 命令,后果天差地别。Shell 工具的权限需要基于命令内容做风险分级:模式匹配(黑名单/白名单)、参数分析、上下文判断。npm install 在开发环境可以自动执行,在 CI 环境可能需要确认。
网络访问的风险在于数据外传和 SSRF。Agent 如果被诱导访问恶意 URL,可能泄漏本地信息或成为跳板。API 工具应该限制目标域名白名单,浏览器工具应该运行在隔离的网络环境中。
沙箱是权限控制在硬件层面的实现。Docker 容器、虚拟机、临时环境在不同层面隔离 Agent 的执行环境。沙箱的选择是安全性和可用性的权衡。Docker 隔离的 Agent 无法访问宿主机文件系统,也无法使用宿主机的开发工具链。临时环境里的 Agent 每次都要重装依赖,增加任务时间。大多数开发场景下,文件系统级别限制(Agent 只能访问项目目录)加上 Shell 命令的白名单和黑名单,是性价比最高的方案。
可观测性是被讨论得最少但最重要的中间层功能。Agent 执行一个 20 分钟的多步任务,产生 80 次工具调用、修改 12 个文件、运行 5 次测试。开发者事后需要知道:每一轮模型请求的输入输出摘要、每次工具调用的参数、结果、耗时和状态、每次文件修改的完整 diff、每次测试运行的结果、每次失败的根因分类(模型生成错误、参数校验失败、执行超时还是权限拒绝)、整个任务的 token 消耗和费用。
这些数据不只是为排错。团队可以分析 Agent 的行为模式,哪个工具调用失败率最高、哪些任务消耗 token 最多、上下文策略需不需要调整。开发者审查时可以快速理解 Agent 做了什么以及为什么这样做,而不是对着 diff 猜意图。
实时可观测性同样重要。开发者在 Agent 执行任务期间不应该只能盯着终端滚动日志。实时 diff 预览、工具调用通知、进度指示,这些看起来是应用层的 UX 问题,实际上依赖中间层提供的事件流和状态推送。中间层只提供同步的请求响应接口的话,应用层就做不出实时进度展示。

中间层的取舍

中间层没有一组正确的配置可以照搬。
上下文窗口用得多,模型判断更全面,但延迟和成本更高。工具定义越丰富,模型能做的事越多,但选错工具的概率也越大。权限越严格,安全性越高,但审批负担越重,Agent 用起来越不顺畅。沙箱越隔离越安全,但 Agent 能完成的任务范围越窄。
这些取舍取决于项目类型、团队规模、风险偏好、代码库复杂度、开发阶段。个人原型项目,权限可以宽松到所有文件操作自动执行。20 人的团队项目,可能需要写文件自动执行但展示 diff,Shell 命令需要确认。银行的生产系统,可能需要所有操作都审批,数据库只有只读权限,所有命令在容器里执行。


本站由 Somnifex 使用 Stellar 1.33.1 主题创建。

本站由 又拍云提供CDN加速/云存储服务

本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。